chore(#10700): re-write Enketo form save workflow - #11256
Conversation
|
I came here to see what this was all about and saw this:
Amazing work Josh!! |
dianabarsan
left a comment
There was a problem hiding this comment.
Very nice rewrite! I think you've definitely taken this into the right direction.
I left some minor comments and possibly found some issues, but nothing major.
| .map(fieldName => ({ fieldName, doc: formData.getSiblingData(fieldName)?.deserializeDoc(config) })) | ||
| .map(({ fieldName, doc }) => ({ fieldName, doc: this.initializeContactSibling(rootOutputDoc, doc)})); |
There was a problem hiding this comment.
Do we need two chained maps here or can everything be done within one iteration?
Wondering if we did something like:
const siblingSave = EnektoContactFormData.SIBLING_FIELD_NAMES.map(async (fieldName) => {
const siblingData = formData.getSiblingData(fieldName)?.deserializeDoc(config);
const siblingDoc = this.initializeContactSibling(rootOutputDoc, siblingData);
rootOutputDoc[fieldName] = await this.getContactSiblingValue(
siblingDoc, rootOutputDoc[fieldName], defaultData[fieldName]
);
});
await Promise.all(siblingSave);| _attachments: formAttachments | ||
| }; | ||
|
|
||
| const siblings = EnektoContactFormData.SIBLING_FIELD_NAMES |
There was a problem hiding this comment.
Please consider moving this sibling handling section into its own function.
| return await this.getContactFromDatasource(Qualifier.byUuid(currentValue._id)); | ||
| } | ||
|
|
||
| private dehydrateContactLineage(contactDoc: Record<string, any>) { |
There was a problem hiding this comment.
I believe the internal name was "minify", instead of "dehydrate". I'm fine with either, but prefer to have a naming convention.
| const newFileAttachments = FileManager | ||
| .getCurrentFiles() | ||
| .map(file => ({ | ||
| name: `${this.USER_FILE_ATTACHMENT_PREFIX}${file.name}`, |
There was a problem hiding this comment.
Old code stripped special characters (UUID fallback for e.g. Devanagari-only names) and rewrote the referencing field values. The specs checking this were dropped without replacement.
|
|
||
| get formDoc() { | ||
| return { _id: this.formId }; | ||
| get formConfig() { |
There was a problem hiding this comment.
Every time we use this getter, it ends up parsing the xml (via the FormConfig constructor). Should we consider cashing here?
| return { _id: this.formId }; | ||
| get formConfig() { | ||
| return new FormConfig( | ||
| { _id: this.formId }, |
There was a problem hiding this comment.
Do we need additional fields on this doc here?
saveReport destructures internalId/xmlVersion from config.doc — with only _id, the emitted report docs seem to lose their form field (on master completeNewReport set form: formId), and binary attachments get named user-file/undefined/...
Description
This PR basically re-writes the doc-saving workflow when an Enekto form (either contact/app/training form) is completed. The existing legacy code suffered from several major problems that made it very difficult to continue developing features on top of it:
The main impetus behind is PR is to provide a better structured base for solving #10700. The hope is that the new functionality in #10923 can be ported on top of the changes in this PR in a way that is way more simple and maintainable.
What is in this PR
The goal was to have this PR be a complete drop-in replacement for the existing functionality. I have done my best to ensure the new code accurately matches the behavior of the old code and there are no new features that I intended to directly support by these changes.
The only behavior difference that I am aware of currently is a new error I added to the save workflow that will fail with a helpful error message when a contact form does not have the contact data in a group that is named for the actual contact type. (So, a person-create form needs to have the contact data in the
persongroup.) This requirement is listed explicitly in our docs and it is implicitly expected by the pre-population logic for setting the initial data onto the contact. Previously it was technically possible to save a contact form with data in a different group. Because our documentation is explicit about how the forms are supposed to be structured, I do not see this as a breaking change. Also, if some does have this issue, they will hit the error and be unable to save those contact forms. It will not just cause weird data issues by failing silently. The fix would just be to update the form config to name the group properly.Added
FormConfigA new
FormConfigclass has been added inwebapp/src/ts/services/form/form-config.tsthat serves as a wrapper for the form doc and its relevant attachments. This FormConfig is retrieved via the XmlFormsService when rendering the form and then can be re-used when saving the form. This wrapper basically centralizes the handling of the form doc and its attachments and avoids the need to retrieve these things multiple times during the render/save process.Added new
EnketoFormDataclassesThe new
EnketoFormDataclasses added inwebapp/src/ts/services/form/form-data.tsare wrappers to encapsulate the XML data model produced when saving an Enekto form. These data classes own the logic for interacting/querying the XML data as well as de-serializing that data to JS objects.Being able to operate against the XML data is useful for queries (either via css selectors or XPaths) which do not have a simple alternative when using normal JS objects. These wrappers help define the available data queries as well as hold on to the
_idreferences for the data to allow for easy interlinking.The xml de-serialization logic from the now-deleted
EnketoTranslationServicewas moved into these EnketoFormData classes. (The JS -> XML serialization logic was just moved to where it was being used in theEnketoPrepoulationDataService.)Move Contact Save functionality into EnketoService
Previously, we had the logic for saving contact forms separated out into the ContactSaveService. I have move this into the EnektoService so now there are only two entry points for saving a form:
EnketoService.saveContactandEnketoService.saveReport. Keeping the code in the same service allows for better alignment between the workflows.As before, there is still probably "too much" code in the EnektoService, but I have balanced things out a bit by deleting duplicate logic and/or moving it into the EnektoFormData classes. The end result is the actual LOC in the enketo.service file has only grown around 30 lines...
Clean up form/enketo service unit tests
A large percentage of the changed lines in this PR are a result of me overhauling the unit tests for the FormService and EnketoService. Previously there was a TON of duplication/leaking between those unit tests (because of how I previously handled separating the FormService from enketo... 😓). The time has come to pay the piper and I have refactored the FormService tests especially to just mock the EnektoService.
AI Disclosure
Basically every single line of the implementation code was hand-written by me. I leveraged Claude a lot for the investigation and minor code structure advice, but ultimately, I really wanted full control/responsibility for porting all the logic line-by-line. Any bugs will be ones that I personally wrote. 👍 😅
I did leverage Claude heavily for the updates to the unit tests. Even there, I have gone through everything line-by-line and made many editorial changes and requested additional tests, etc. There is a lot of lines changed, so it is possible we missed some stuff, but I am highly confident the functionality is well tested and the tests are very maintainable going forwards.
Code review checklist
can_view_old_navigationpermission to see the old design. Test it has appropriate design for RTL languages.Compose URLs
If Build CI hasn't passed, these may 404:
License
The software is provided under AGPL-3.0. Contributions to this project are accepted under the same license.